Two-Way Main-Renderer IPC
See the Electron-Documentation for a more indepth explanation of the reasons for, and how the IPC works between the Main process, the Renderer process, and why the preload step is required.
Unlike typical Fable.Remoting, the entire stack is compiled to JavaScript, so there is no
requirement to define your API types in a Shared project. You can choose to do this or not.
Example
The Two-Way Main-Renderer IPC channel allows the client/renderer process to
send a message to the main process, and to then receive a response asynchronously.
This is required due to the Renderer processes not having access to the Node.js API, nor
the full Electron API.
A full example is provided below:
- Shared.fs
- main.fs
- preload.fs
- renderer.fs
open Fable.Core.JS // Promise type
type ExampleRouting = {
SayHelloWorld: string -> Promise<Result<string, unit>>
}
open Fable.Electron
open Fable.Core
open Fable.Core.JsInterop
open Fable.Core.JS
open Node.Api
open Node.Base
open Fable.Electron.Main
open Fable.Electron.Remoting.Main
if SquirrelStartup.started then
app.quit()
let createWindow() =
let mainWindowOptions =
BrowserWindowConstructorOptions(
width = 800,
height = 600,
webPreferences = WebPreferences(
preload = path.join(__dirname, "preload.js")
)
)
let mainWindow = BrowserWindow(mainWindowOptions)
if isNullOrUndefined MAIN_WINDOW_VITE_DEV_SERVER_URL
then mainWindow.loadFile(path.join(__dirname, $"../renderer/{MAIN_WINDOW_VITE_NAME}/index.html"))
else mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
|> ignore
mainWindow.webContents.openDevTools(Enums.WebContents.OpenDevTools.Options.Mode.Right)
app.whenReady().``then``(fun () ->
let api: Shared.ExampleRouting = {
SayHelloWorld = fun text -> promise {
if text = "hello" then
return Ok <| text + " world!"
else
return Error()
}
}
Remoting.createIpc ()
|> Remoting.fromValue api
createWindow()
app.onActivate(fun _ ->
if BrowserWindow.getAllWindows().Length = 0 then
createWindow()
)
)
|> ignore
app.onWindowAllClosed(fun () ->
if not Node.Api.process.platform.IsDarwin then
app.quit()
)
open Fable.Electron.Playground.Shared
open Fable.Electron.Remoting.Preload
Remoting.createIpc ()
|> Remoting.buildTwoWayBridge<ExampleRouting>
open Fable.Core.JsInterop
open Fable.Electron.Remoting.Renderer
open Browser.Dom
importSideEffects "./index.css"
console.log "This message is being logged by 'renderer.js', included via VITE"
let api =
Remoting.createIpc ()
|> Remoting.buildProxySender<Shared.ExampleRouting>
(api.SayHelloWorld "hello").``then``(function
| Ok v -> console.log v
| Error _ -> console.log "Didn't say hello back :(")
|> ignore
The API for Fable.Electron.Remoting is highly likely to change
according to user preference at the beginning.
IpcMainEvent
The shared contract should remain focused on renderer-callable arguments.
If you need IpcMainEvent inside Main handlers, provide a factory function
and use Remoting.fromIpcMainEvent.
Do not add IpcMainEvent to the shared record function signatures.
Use explicit context injection on the Main side instead.
type ExampleRouting = {
SayHelloWorld: string -> Promise<Result<string, unit>>
}
let api =
Remoting.createIpc ()
|> Remoting.buildProxySender<Shared.ExampleRouting>
(api.SayHelloWorld "hello").``then``(function
| Ok v -> console.log v
| Error _ -> console.log "Didn't say hello back :("
|> ignore
let api (event: IpcMainEvent) : Shared.ExampleRouting = {
SayHelloWorld = fun (text: string) -> promise {
if text = "hello" then
return Ok <| $"Window {event.sender.id} said: {text} world!"
else
return Error()
}
}
Remoting.createIpc ()
|> Remoting.fromIpcMainEvent api
If you do not need event context, keep using Remoting.fromValue.